16.1. C 语言编程入门

我们先来看一个“hello world”程序,其中包含一个从数学库调用函数的示例。在 表 1 中,我们将该程序的 C 版本与 Java 版本进行了比较。C 版本可能放在名为 hello.c 的文件中(.c 是 C 源代码文件的后缀约定),而 Java 版本可能放在名为 HelloWorld.java 的文件中。

表 1. Java 和 C 中小程序的语法比较。C 版本 和 Java 版本 均可下载。

Java version (HelloWorld.java)C version (hello.c)
/*
The Hello World Program in Java
/

/
Java Math library /
import java.lang.Math;

/
define a HelloWorld class /
class HelloWorld {

/
main method definition: */
public static void main(String[] args){

System.out.println("Hello World");
System.out.println("sqrt(4) is "
+ Math.sqrt(4));
}
}
/*
The Hello World Program in C
/

/
C math and I/O libraries /
#include <math.h>
#include <stdio.h>



/
main function definition: */
int main(void) {

printf("Hello World\n");
printf("sqrt(4) is %f\n", sqrt(4));

return 0; // main returns value 0
}

请注意,尽管语言语法不同,但该程序的两个版本都具有相似的结构和语言结构。

一些句法相似之处包括:

注释:

  • Java 和 C 中的多行注释以 /* 开头,以 */ 结尾,单行注释以 // 开头。

语句:

  • C 和 Java 中的语句以“;”结尾。

代码块:

  • Java 和 C 均使用 { 和 } 括住相关代码块(例如,函数体和循环体)。良好的编程风格包括在块内缩进语句。

一些主要的区别包括:

导入库代码:

  • 在 Java 中,使用import来包含(导入)库。
  • 在 C 语言中,使用 #include 来包含(导入)库。所有 #include 语句都出现在程序顶部,函数体之外。

main函数:

  • Java 和 C 都定义了 main 函数,它们是程序运行时最先执行的函数。在 C 中,只定义了一个 main 函数,它在 C 程序执行时自动调用。在 Java 中,会执行 JVM 上运行的类的 public static void main 方法。
  • Java 是一种纯面向对象的语言,因此所有代码都必须是类的一部分(本例中为HelloWorld)。 main 函数在类HelloWorld中定义为public static方法(public static void main(String[] args))。按照惯例, main 在 Java 中是一个void函数,并传递了一个命令行参数字符串数组。
  • C 是一种纯命令式和过程式语言,因此 C 中没有类。因此,所有函数都是在类定义之外定义的(C 中没有类定义)。在 C 中,int main(void){ } 定义主函数。void 表示它不希望接收参数。后面的部分将展示 main 如何接受参数来接收命令行参数。
  • C 程序必须有一个名为 main 的函数,并且其返回类型必须是int。C 中的 main 函数可以选择接受一个字符串列表作为参数,每个命令行参数一个字符串(类似于 Java),但在其最简单的形式中, main 没有参数。在第 2 章中,我们展示了定义为接受命令行参数的 main
  • main 函数有一个明确的return语句来返回一个int值(按照惯例,如果主函数成功执行而没有错误,则main返回0)。

输出:

  • 在 Java 中,System.outprintprintln 方法可用于打印字符串。+ 运算符可用于将值连接在一起以创建更复杂的字符串(例如,"sqrt(4) is " + Math.sqrt(4))。System.out 还有一个 printf 方法,用于打印带有参数的格式字符串。格式字符串中占位符的值以逗号分隔的参数值列表形式显示。例如,表 1 中对 System.out.println 的第二次调用可以替换为等效调用 System.out.printf("sqrt(4) is %f%n", Math.sqrt(4)), 其中 Math.sqrt(4)的值将代替格式字符串中的 %f 占位符进行打印,而 %n(或 \n)用于指定换行符。Java 还具有可用于格式化不同类型值的类。
  • 在 C 语言中,printf 函数像 Java 的 System.out.printf 方法一样打印格式化的字符串(例如,sqrt(4) 的值将打印在格式字符串参数中的 %f 占位符的位置,而 \n 指定换行符)。

printf 函数用于打印格式字符串和简单字符串值(C 没有类似于 Java 的 System.out.println 的单独函数)。C 的 printf 函数也不会自动在末尾打印换行符。因此,当需要在输出中使用换行符时,C 程序员需要在格式字符串中明确指定换行符(\n)。

16.1.1. 编译和运行 C 程序

Java 程序在 Java 虚拟机 (JVM) 上运行。JVM 是直接在底层计算机系统上运行的程序。要运行 Java 程序,首先需要 Java 编译器 (javac) 将其源代码 (HelloWorld.java) 形式编译 (翻译) 为 Java 字节码形式。例如 ($ 是 Linux shell 提示符):

$ javac HelloWorld.java

如果成功,javac 将创建一个新文件 HelloWorld.class,其中包含 JVM 可以运行的程序的 Java 字节码转换。例如:

$ java HelloWorld

JVM 是一种可以直接在底层系统上运行的程序(这种形式称为 二进制可执行文件),并以它运行的 Java 类作为输入(图 1)。Java 字节码的可移植性很高,它可以在任何具有 JVM 的计算机系统上运行。但是,由于 Java 字节码不直接在底层计算机系统上运行,因此 Java 程序的运行效率可能不如直接在底层系统上运行的程序。

Execution of a Java program by the JVM.

图 1. Java 程序被编译为 Java 字节码,由 JVM 执行,JVM 是在底层系统(操作系统和硬件)上运行的二进制可执行程序

要运行 C 程序,必须先将其转换成计算机系统可以直接执行的形式。C 编译器与 Java 编译器类似,是一个将 C 源代码转换成计算机系统可以直接执行的二进制可执行文件形式的程序。二进制可执行文件由一系列计算机可以运行的明确定义的格式的 0 和 1 组成;与需要 JVM 才能运行的 Java 字节码不同,二进制可执行文件直接在底层系统上运行。

例如,要在 Unix 系统上运行 C 程序hello.c,必须先由 C 编译器(例如 GNU C 编译器、GCC)编译 C 代码,生成二进制可执行文件(默认名为“a.out”)。然后可以直接在系统上运行该程序的二进制可执行版本(图 2):

$ gcc hello.c
$ ./a.out

(请注意,某些 C 编译器可能需要明确告知链接数学库:-lm):

$ gcc hello.c -lm

C program text goes to the C compiler, which converts it into an executable sequence of zeroes and ones.  The format of the executable sequence can be run by the underlying system. 图 2. C 编译器 (gcc) 将 C 源代码编译为二进制可执行文件 (a.out)。底层系统(操作系统和硬件)直接执行 a.out 文件来运行程序。

详细步骤

一般来说,以下序列描述了在 Unix 系统上编辑、编译和运行 C 程序的必要步骤:

  1. 使用 文本编辑器 (例如 vim)编写 C 源代码程序并将其保存在文件中(例如 hello.c):
	$ vim hello.c
  1. 将源代码编译为可执行文件,然后运行它。使用 gcc 进行编译的最基本语法是:
	$ gcc <input_source_file>

如果编译没有错误,编译器将创建一个名为a.out的二进制可执行文件。编译器还允许您使用 -o标志指定要生成的二进制可执行文件的名称:

$ gcc -o <output_executable_file> <input_source_file>

例如,此命令指示gcchello.c编译为名为hello的可执行文件:

$ gcc -o hello hello.c

我们可以使用 ./hello 调用可执行程序:

$ ./hello

对 C 源代码(hello.c 文件)所做的任何更改都必须使用 gcc 重新编译,以生成新版本的 hello。如果编译器在编译过程中检测到任何错误,则不会创建/重新创建 ./hello 文件(但请注意,上次成功编译的文件的旧版本可能仍然存在)。

通常在使用 gcc 进行编译时,您需要包含几个命令行选项。例如,这些选项可启用更多编译器警告并使用额外的调试信息构建二进制可执行文件:

$ gcc -Wall -g -o hello hello.c

由于 gcc 命令行可能很长,因此经常使用 make 实用程序来简化 C 程序的编译以及清理由 gcc 创建的文件。使用 make 和编写 Makefile 是您在积累 C 编程经验时将培养的重要技能。 我们将在编译步骤末尾更详细地介绍如何使用 C 库代码进行编译和链接。

16.1.2. 变量和 C 数字类型

与 Java 一样,C 使用变量作为保存数据的命名存储位置。考虑程序变量的作用域类型对于理解程序运行时的语义非常重要。变量的作用域定义了变量何时具有意义(即在程序中的何时何地可以使用它)及其生命周期(即它可以在整个程序运行期间或仅在函数激活期间存在)。变量的类型定义了它可以表示的值的范围以及在对其数据执行操作时如何解释这些值。

在 Java 和 C 中,所有变量都必须先声明才能使用。要在 C 中声明变量,请使用以下语法:

type_name variable_name;

一个变量只能有一个类型。基本 C 类型包括 charintfloatdouble。按照惯例,C 变量应在其范围的开头({ } 块的顶部)声明,并且应在该范围内的任何 C 语句之前声明。

下面是一个示例 C 代码片段,展示了一些不同类型的变量的声明和使用。我们将在示例之后更详细地讨论类型和运算符。

varsin.c

{
    /* 1. Define variables in this block's scope at the top of the block. */

    int x; // declares x to be an int type variable and allocates space for it

    int i, j, k;  // can define multiple variables of the same type like this

    char letter;  // a char stores a single-byte integer value
                  // it is often used to store a single ASCII character
                  // value (the ASCII numeric encoding of a character)
                  // a char in C is a different type than a string in C

    float winpct; // winpct is declared to be a float type
    double pi;    // the double type is more precise than float

    /* 2. After defining all variables, you can use them in C statements. */

    x = 7;        // x stores 7 (initialize variables before using their value)
    k = x + 2;    // use x's value in an expression

    letter = 'A';        // a single quote is used for single character value
    letter = letter + 1; // letter stores 'B' (ASCII value one more than 'A')

    pi = 3.1415926;

    winpct = 11 / 2.0; // winpct gets 5.5, winpct is a float type
    j = 11 / 2;        // j gets 5: int division truncates after the decimal
    x = k % 2;         // % is C's mod operator, so x gets 9 mod 2 (1)
}

16.1.3. C类型

与 Java 不同,C 没有定义复杂数据类型的大量类库。相反,C 支持一小组内置数据类型,并且它提供了几种方法供程序员构建基本类型集合(数组和结构)。通过这些基本构建块,C 程序员可以构建复杂的数据结构。

C 定义了一组用于存储数值的基本类型。以下是不同 C 类型的数字文字值的一些示例:

8     // the int value 8
3.4   // the double value 3.4
'h'   // the char value 'h' (its value is 104, the ASCII value of h)

char 类型存储数值。但是,程序员经常使用它来存储 ASCII 字符的值。在 C 中,字符文字值指定为单引号之间的单个字符。

C 不支持字符串类型,但程序员可以从 char 类型创建字符串,并且 C 支持构造值数组,我们将在后面的章节中讨论。但是,C 支持在程序中表达字符串文字值的方式:字符串文字是双引号之间的任何字符序列。C 程序员经常将字符串文字作为格式字符串参数传递给 printf

printf("this is a C string\n");

Java 和 C 都支持字符串和字符类型值。通常,Java char 值是 16 位 unicode 值,而 C 是 8 位 ascii 值。

在 Java 和 C 中,字符串和 char 是两种非常不同的类型,它们的求值方式也不同。通过对比包含一个字符的 C 字符串文字和 C char 文字可以说明这种差异。例如:

'h'  // this is a char literal value   (its value is 104, the ASCII value of h)
"h"  // this is a string literal value (its value is NOT 104, it is not a char)

我们将在本章后面的 字符串 部分更详细地讨论 C 字符串和 char 变量。在这里,我们主要关注 C 的数字类型。

C 数字类型

C 支持几种不同的类型来存储数值。这些类型在它们所表示的数值的格式上有所不同。例如,floatdouble 类型可以表示实数,int 表示有符号整数值,而 unsigned int 表示无符号整数值。实数是带有小数点的正值或负值,例如 -1.230.0056。有符号整数存储正、负或零整数值,例如 -33303456。无符号整数存储严格非负的整数值,例如 01234

C 的数字类型在它们可以表示的值的范围和精度方面也有所不同。值的范围或精度取决于与其类型关联的字节数。与字节数较少的类型相比,字节数较多的类型可以表示更大范围的值(对于整数类型)或更高精度的值(对于实数类型)。

表 2 显示了存储字节数、存储的数值类型以及如何为各种常见的 C 数字类型声明变量(请注意,这些是典型大小 - 具体字节数取决于硬件架构)。

表 2. C 数值类型

Type nameUsual sizeValues storedHow to declare
char1 byteintegerschar x;
short2 bytessigned integersshort x;
int4 bytessigned integersint x;
long4 or 8 bytessigned integerslong x;
long long8 bytessigned integerslong long x;
float4 bytessigned real numbersfloat x;
double8 bytessigned real numbersdouble x;

C 还提供整数数字类型的无符号版本(charshortintlonglong long)。要将变量声明为无符号,请在类型名称前添加关键字unsigned。例如:

int x;           // x is a signed int variable
unsigned int y;  // y is an unsigned int variable

C 标准未指定 char 类型是有符号的还是无符号的。因此,某些实现可能将 char 实现为有符号整数值,而其他实现则实现为无符号整数值。如果要使用 char 变量的无符号版本,则明确声明 unsigned char 是一种良好的编程习惯。

每种 C 类型的确切字节数可能因架构而异。表 2 中的大小是每种类型的最小(和常见)大小。您可以使用 C 的 sizeof 运算符打印给定机器上的确切大小,该运算符以类型的名称作为参数并计算用于存储该类型的字节数。例如:

printf("number of bytes in an int: %lu\n", sizeof(int));
printf("number of bytes in a short: %lu\n", sizeof(short));

sizeof 运算符的计算结果为无符号长整型值,因此在调用 printf 时,使用占位符 %lu 来打印其值。在大多数体系结构中,这些语句的输出将是:

number of bytes in an int: 4
number of bytes in a short: 2

算术运算符

算术运算符用于组合数字类型的值。运算的结果类型取决于操作数的类型。例如,如果两个 int 值与算术运算符组合,则结果类型也是整数。

当运算符组合两种不同类型的操作数时,C 会执行自动类型转换。例如,如果 int 操作数与 float 操作数组合,则在应用运算符之前,整数操作数会先转换为其浮点数,并且运算结果的类型为 float

以下算术运算符可用于大多数数字类型操作数:

  • 加法 (+) 和减法 (-)

  • 乘法(*)、除法(/)和模数(%):

    模运算符()只能采用整数类型的操作数(intunsigned intshort等等)。

    如果两个操作数都是int类型,则除法运算符(/)执行整数除法(结果值为int,除法运算会截断小数点后的所有内容)。例如,8/3的计算结果为2

    如果一个或两个操作数都是“浮点数”(或“双精度数”),则/执行实数除法并计算结果为“浮点数”(或“双精度数”)。例如,8 / 3.0的计算结果约为2.666667

  • 赋值(=):

    变量 = 表达式的值;//例如,x = 3 + 4;

  • 带更新的赋值(+=-=*=/=%=):

    变量 op= 表达式;//例如,x += 3;是 x = x + 3 的简写;

  • 增量(++)和减量(--):

    变量++; // 例如,x++; 将 x + 1 的值赋给 x

pre- vs. post-increment

运算符 ++variable 和 variable++ 都是有效的,但它们的评估方式略有不同:

  • ++x:先增加 x,然后使用它的值。

  • x++:首先使用 x 的值,然后增加它。

    在许多情况下,使用哪种语句并不重要,因为语句中未使用递增或递减变量的值。例如,这两个语句是等效的(尽管第一个语句是此语句最常用的语法):

    x++;
    ++x;
    

    在某些情况下,上下文会影响结果(当语句中使用递增或递减变量的值时)。例如:

    x = 6;
    y = ++x + 2; // y is assigned 9: increment x first, then evaluate x + 2 (9)
    
    x = 6;
    y = x++ + 2; // y is assigned 8: evaluate x + 2 first (8), then increment x
    

    像上例这样使用带有增量运算符的算术表达式的代码通常难以阅读,而且很容易出错。因此,最好避免编写这样的代码;相反,按照您想要的顺序编写单独的语句。例如,如果您想先增加x,然后将x + 1赋值给y,只需将其写成两个单独的语句即可。

    而不要这样写:

    y = ++x + 1;
    

    将其写成两个单独的语句:

    x++;
    y = x + 1;